# Encapsulates operations on the repository.
import pysvn
import re
from tkMessageBox import *
from status_ui import *
from util import do_nothing
import shutil
from glob import glob
import watchdog
from watchdog.observers import Observer
import os, sys, re

# TODO: dedup
def find_google_code_password(ev):
  webbrowser.open("https://code.google.com/hosting/settings",
      autoraise=True)

class Credentials(object):
  def __init__(self, username, password):
    self.username = username
    self.password = password

class Repository(object):
  def __init__(self, url, credentials=None, client = None):
    if client is None:
      client = pysvn.Client()
    self.client = client
    client.callback_ssl_client_cert_password_prompt=self.get_password
    client.callback_ssl_server_trust_prompt=self.trust_prompt
    client.callback_get_login = self.get_login
    client.callback_notify = self.notify
    client.callback_get_log_message = self.get_log_message
    self.url = url
    if self.url[-1] != '/':
      self.url += '/'
    self.base_url = re.sub('/trunk/', '/', url)
    self.credentials = credentials
    self.responder = None

  def diff_revs(self, from_rev, to_rev):
    "Determines the file title-level differences between revisions."
    # TODO
    pass

  def notify(self, event_dict):
    if event_dict['action'] == pysvn.wc_notify_action.add:
      self.responder.put("Adding " + event_dict['path'])
    elif event_dict['action'] == pysvn.wc_notify_action.update_add:
      self.responder.put("Adding " + event_dict['path'])
    elif event_dict['action'] == pysvn.wc_notify_action.update_delete:
      self.responder.put("Deleting " + event_dict['path'])
    elif event_dict['action'] == pysvn.wc_notify_action.update_update:
      self.responder.put("Updating " + event_dict['path'])
    else:
      self.responder.put("Operating on " + event_dict['path'])
    # TODO: add more message types

  def get_password(self, realm, may_save):
    self.responder.put("Logging in...")
    credentials = self.credentials
    if not credentials:
      # TODO: only ask for password?
      # TODO: remember e-mail?
      # TODO: remember these credentials only if login is
      # successful, and re-think this logic...
      credentials = self.responder.get_password()
      if not credentials:
        return True, '', False
    return True, self.credentials.password, False
    # TODO: make last one True?

  def get_login(self, realm, uname, may_save):
    messages.put("Logging in...")
    credentials = self.credentials
    if not credentials:
      # TODO: dedup with above
      credentials = self.responder.get_credentials()
    return (True, credentials.username, credentials.password,
      False) # TODO: make last one True?

  def trust_prompt(self, trust_dict):
    # TODO: set values carefully
    return True, 3, True

  def get_log_message(self):
    # TODO: devise a better way to pass messages...
    message = self.message
    self.message = None
    if message is None:
      message = self.responder.get_commit_message()
    return not not message, message

  def set_message(self, message):
    self.message = message

  def check_out_wc(self, path, messages):
    "Checks out a working copy to the given path. \
     Returns a corresponding WorkingCopy object."
    # TODO: store username and password in a better way
    url = self.url
    messages.put("Checking out working copy from:")
    messages.put(url)
    self.messages = messages
    self.client.checkout(url, path)
    self.messages = None
    messages.put("Checkout complete!")

    return WorkingCopy(path, self)

  def check_out_item(self, url, path):
    # TODO
    pass

  def get_tag_url(self, branch):
    return "%s/tags/%s/" % (self.base_url, branch);

  def get_branch_split_rev(self, branch):
    """
    Finds the main revision at which a branch was split,
    as well as the branch's most revent revision.
    """
    client = pysvn.Client()
    # Get log messages
    messages = client.log(self.get_tag_url(branch), \
      discover_changed_paths = True)

    # Now, find the latest version that our working copy is at.
    # We require that all files in the working copy have the
    # same revision.
    # EasySVN requires updates after commits, and does not currently
    # support updating to any revision but HEAD, so the working
    # copy should never have mixed revisions.
    #
    # TODO: review this assumption in the future!
    statuses = client.status

    # Find the earliest path copy operation, and what revision it
    # was copied at.
    branch_revision = None
    last_rev = None

    # For tiny branches, this really shouldn't take long!
    print "Log Messages"
    for msg in messages:
      log_rev = msg['revision']
      print "Log rev: %s" % log_rev

      # Get the last rev
      # TODO: optimize by only reading first message?
      if log_rev and log_rev.kind == pysvn.opt_revision_kind.number and log_rev.number > last_rev:
        last_rev = log_rev.number

      # Find any revs that were copied over
      for path in msg['changed_paths']:
        # If we created the branch folder at this revision, let's
        # use it.
        if path['path'] == "/tags/" + branch and path['action'] == 'A':
          branch_revision = path['copyfrom_revision'].number
          print "Branch revision is %d" % branch_revision

    #print "Branch revision: %d"
    return branch_revision, last_rev

  def get_branch_list(self, responder):
    self.responder = responder
    # TODO: rename to get_tag_list
    # TODO: reuse the client object
    # List directory
    url = "%s/tags" % self.base_url
    print url
    dirs = self.client.list(url, \
                            dirent_fields = pysvn.SVN_DIRENT_KIND)

    print dirs
    for d, l in dirs:
      print os.path.basename(d.repos_path)
      print d.kind
    branches = [os.path.basename(dirent.repos_path) for dirent, lock_info \
      in dirs if dirent.kind == pysvn.node_kind.dir]
    # TODO: do the following in a better way...
    branches = [x for x in branches if x != 'tags']

    self.responder.set_completed()
    return branches


class GoogleCodeRepository(Repository):
  GOOGLE_CODE_URL = "https://%s.googlecode.com/svn/trunk/"
  def __init__(self, project_name, credentials=None):
    super(GoogleCodeRepository, self).__init__(
      self.GOOGLE_CODE_URL % project_name, credentials)
    self.project_name = project_name

class WorkingCopy(watchdog.events.FileSystemEventHandler):
  def __init__(self, path, repository=None):
    self.path = path
    self.repository = repository
    self.on_change = do_nothing

    # Create the repository object if none was provided.
    if repository is None:
      client = pysvn.Client()
      self.repository = Repository(client.root_url_from_path(path), client=client)

    # TODO make this nicer...?
    self.client = self.repository.client

    # TODO: put the Observer elsewhere?
    # TODO: ignore changes to the filesystem that result
    # from working copy Subversion actions.
    self.observer = Observer()
    self.observer.schedule(self, path, recursive=True)
    self.observer.start()

  def on_any_event(self, event):
    # TODO: report specific path name
    print "File system changed!"
    self.on_change()

  def status(self):
    "Get the repository status."
    # TODO
    return self.client.status(self.path)

  def action_wrapper(self, responder, thunk):
    "Sets up the responder and observer, and calls thunk."
    self.repository.responder = responder
    self.observer.unschedule_all()

    try:
      thunk()

      responder.set_completed()
      self.on_change()
    except Exception as e:
      responder.put("Error: %s" % e)
    finally:
      self.observer.schedule(self, self.path, recursive=True)

  def addItem(self, path, responder):
    "Adds files."
    def thunk(): self.client.add(path)
    self.action_wrapper(responder, thunk)

  def ignoreItem(self, path, responder):
    "Ignores files."
    def thunk():
      parent_dir = os.path.dirname(path)
      base_name = os.path.basename(path)
      prop_vals = self.client.propget('svn:ignore', parent_dir)
      for prop_val in prop_vals.itervalues():
        ignored_files = prop_val.split(re.compile('\n|\r|\r\n'))
        if base_name not in ignored_files:
          ignored_files.append(base_name)
          self.client.propset('svn:ignore', '\n'.join(ignored_files), path)
      # TODO: resolve ignore property conflicts by:
      #  * checking for added files that are ignored, and un-ignoring them.
      #  * adding all other ignores.
    self.action_wrapper(responder, thunk)

  def deleteItem(self, path, responder):
    "Deletes items."
    def thunk(): self.client.remove(path)
    self.action_wrapper(responder, thunk)
    # TODO: provide option to keep local file

  def revertItem(self, path, responder):
    "Reverts an item."
    def thunk(): self.client.revert(path)
    # TODO: use Python decorators for action wrappers?
    self.action_wrapper(responder, thunk)

  def useMyItem(self, path, responder):
    def thunk():
      shutil.copy(path + ".mine", path)
      self.client.resolved(path)
    self.action_wrapper(responder, thunk)

  def useTheirItem(self, path, responder):
    # Check for binary files!!!
    # Copy over the most recent version, instead of doing an update,
    # which may involve network traffic.

    def thunk():
      chosen_path = None
      chosen_rev = None
      for rev_path in glob(path + ".r*"):
        rev = int(re.match('\d+$', rev))
        if chosen_path is None or rev > chosen_rev:
          chosen_path = rev_path

      # TODO: use copy2, copying stat information as well?
      shutil.copy(chosen_path, path)

      self.client.resolved(path)
    self.action_wrapper(responder, thunk)

  def resolveItem(self, path, responder):
    "Marks an item as resolved."
    def thunk(): self.client.resolved(path)
    # TODO: use decorators?
    self.action_wrapper(responder, thunk)

  def revert(self, path, responder):
    "Discards changes to the given path."
    def thunk(): self.client.revert(path, recurse=True)
    self.action_wrapper(responder, thunk)

  def revert_all(self, responder):
    "Discards all changes."
    def thunk(): self.client.revert(self.path, recurse=True)
    self.action_wrapper(responder, thunk)

  def commit(self, commit_message, responder):
    "Commits changes to the Repository."
    def thunk():
      # Check in the latest code.
      self.client.checkin(self.path, commit_message)

      # Now, force all files in the working copy to be on the latest
      # revision, not just the committed ones.  This prevents multiple-
      # revision working copies.
      self.client.update(self.path)
    self.action_wrapper(responder, thunk)

  def shelve(self, new_branch, message, responder):
    "Stashes changes, switches branches, and applies the changes."
    # TODO: hacky
    def thunk():
      self.repository.set_message(message)
      self.client.copy(self.path, self.repository.get_tag_url(new_branch))
    self.action_wrapper(responder, thunk)

  def unshelve(self, other_branch, responder):
    "Merges another branch with the current one."
    # TODO remove all the direct references to repository properties
    def thunk():
      trunk_rev, branch_rev = self.repository.get_branch_split_rev(other_branch)
      if trunk_rev is None or branch_rev is None:
        # TODO: don't show GUI stuff...
        raise Exception("Could not find branch point for branch %s.  Maybe it doesn't exist?" % other_branch)

      # Now, merge the changes...
      responder.put("Merging differences from %s:%d to %s:%d" % \
          (self.repository.base_url + 'trunk', trunk_rev, \
          self.repository.get_tag_url(other_branch), branch_rev))
      self.client.merge(self.repository.url, \
        pysvn.Revision(pysvn.opt_revision_kind.number, trunk_rev), \
        self.repository.get_tag_url(other_branch), \
        pysvn.Revision(pysvn.opt_revision_kind.number, branch_rev), \
        self.path)
    self.action_wrapper(responder, thunk)

  def update(self, responder, to_rev=None):
    # TODO: a nicer way to store the responder reference...?
    # TODO: support to_rev, and think about implications of mixed-revision
    # working copies.
    "Updates the working copy to the given revision."
    def thunk(): self.client.update(self.path)
    self.action_wrapper(responder, thunk)

  def on_path_change(self, path):
    "Update the status of the file."
    # TODO: does this belong here?
    pass
